이 노트북은 제이크 반더플라스(Jake VanderPlas)의 A Whirlwind Tour of Python(OReilly Media, 2016)를 기반으로 만들어졌습니다. 이 내용은 CC0 라이센스를 따릅니다. 전체 노트북의 목록은 https://github.com/rickiepark/WhirlwindTourOfPython 에서 볼 수 있습니다.
프로그래머로서의 기술에 상관없이 코딩 실수는 하기 마련입니다. 여기에는 세 종류의 실수가 있습니다:
여기에서는 어떻게 실행시 에러를 깔끔하게 다루는지 배웁니다. 파이썬은 예외처리 핸들링 프레임워크로 실행시 에러를 처리합니다.
print(Q)
NameErrorTraceback (most recent call last) <ipython-input-1-cbf1bd89097d> in <module> ----> 1 print(Q) NameError: name 'Q' is not defined
또는 정의되지 않은 연산을 시도할 때입니다:
1 + 'abc'
TypeErrorTraceback (most recent call last) <ipython-input-2-a51a3635a212> in <module> ----> 1 1 + 'abc' TypeError: unsupported operand type(s) for +: 'int' and 'str'
또는 수학적으로 계산할 수 없는 연산을 시도할 때입니다:
2 / 0
ZeroDivisionErrorTraceback (most recent call last) <ipython-input-3-8b4ac6d3a3e1> in <module> ----> 1 2 / 0 ZeroDivisionError: division by zero
또는 존재하지 않는 원소에 접근할 때입니다:
L = [1, 2, 3]
L[1000]
IndexErrorTraceback (most recent call last) <ipython-input-4-354067ebdc84> in <module> 1 L = [1, 2, 3] ----> 2 L[1000] IndexError: list index out of range
각 경우에 파이썬은 단순히 에러가 일어난 것을 알려주는 것 뿐만 아니라 정확히 무엇이 잘못 되었는지 에러가 발생한 코드의 위치는 어디인지의 정보를 포함한 의미있는 예외를 발생시킵니다. 이런 에러의 의미를 사용하면 코드에 있는 문제를 찾아가는데 매우 도움이 됩니다.
try
와 except
¶실행시 예외를 다루기 위한 도구는 try
...except
절입니다.
기본 구조는 다음과 같습니다:
try:
print("먼저 이 라인이 실행됩니다")
except:
print("에러가 발생할 때 실행됩니다")
먼저 이 라인이 실행됩니다
첫 번째 블럭에서 에러가 발생하지 않았기 때문에 두 번째 블럭은 실행되지 않습니다.
try
블럭안에 잘못된 코드를 넣으면 어떻게 되는지 보겠습니다:
try:
print("다음 코드를 실행합니다:")
x = 1 / 0 # ZeroDivisionError
except:
print("무언가 잘못되었습니다!")
다음 코드를 실행합니다: 무언가 잘못되었습니다!
try
문 안에서 에러가 발생하면(여기서는 ZeroDivisionError
) 캐치되어 except
문이 실행됩니다.
함수나 코드에서 사용자의 입력을 체크하는데 자주 사용하는 방법입니다. 예를 들어 0으로 나누는 에러를 캐치하여 $10^{100}$와 같이 아주 충분히 큰 값을 반환하는 함수를 만들 수 있습니다:
def safe_divide(a, b):
try:
return a / b
except:
return 1E100
safe_divide(1, 2)
0.5
safe_divide(2, 0)
1e+100
이 함수에는 교묘한 문제가 있습니다. 다른 종류의 예외가 발생하면 어떻게 될까요? 가령, 다음은 예상하지 못한 상황입니다:
safe_divide (1, '2')
1e+100
정수와 문자열을 사용해 나눗셈을 하면 TypeError
가 발생됩니다. 이 에러도 과도하게 캐치하여 ZeroDivisionError
로 간주하는 셈이 됩니다!
이런 이유 때문에 명시적으로 캐치할 예외를 지정하는 것이 좋습니다:
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
return 1E100
safe_divide(1, 0)
1e+100
safe_divide(1, '2')
TypeErrorTraceback (most recent call last) <ipython-input-13-cbb3eb91a66d> in <module> ----> 1 safe_divide(1, '2') <ipython-input-11-57f0d324952e> in safe_divide(a, b) 1 def safe_divide(a, b): 2 try: ----> 3 return a / b 4 except ZeroDivisionError: 5 return 1E100 TypeError: unsupported operand type(s) for /: 'int' and 'str'
이제 0으로 나누는 에러만 캐치하고 나머지 에러는 그냥 통과시킵니다.
raise
¶파이썬을 사용할 때 예외에 담긴 정보가 얼마나 유용한지 보았습니다. 여러분이 작성한 코드에서도 유용한 예외는 동일한 가치가 있습니다. 이렇게 하면 코드를 사용할 사용자(무엇보다도 자기자신!)가 에러가 발생된 원인을 쉽게 찾을 수 있습니다.
자기자신의 예외를 발생시키는 방법은 raise
문을 사용하는 것입니다. 예를 들어:
raise RuntimeError("my error message")
RuntimeErrorTraceback (most recent call last) <ipython-input-14-b1834d213d3b> in <module> ----> 1 raise RuntimeError("my error message") RuntimeError: my error message
이에 대한 유용한 예를 위해서 앞서 정의한 fibonacci
함수로 돌아가 보죠:
def fibonacci(N):
L = []
a, b = 0, 1
while len(L) < N:
a, b = b, a + b
L.append(a)
return L
이 함수에서 한 가지 문제점은 입력값이 음수일 수 있다는 것입니다.
음수가 입력되어도 이 함수에서 어떤 에러도 발생시키지는 않습니다. 하지만 음수 N
을 지원하지 않는다는 점을 사용자에게 알리고 싶습니다.
관례적으로 잘못된 파라미터 값에의해 발생한 에러는 ValueError
를 발생시킵니다:
def fibonacci(N):
if N < 0:
raise ValueError("N은 음수가 아니어야 합니다")
L = []
a, b = 0, 1
while len(L) < N:
a, b = b, a + b
L.append(a)
return L
fibonacci(10)
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
fibonacci(-10)
ValueErrorTraceback (most recent call last) <ipython-input-17-f1ae0a8066f0> in <module> ----> 1 fibonacci(-10) <ipython-input-15-2733925b15d0> in fibonacci(N) 1 def fibonacci(N): 2 if N < 0: ----> 3 raise ValueError("N은 음수가 아니어야 합니다") 4 L = [] 5 a, b = 0, 1 ValueError: N은 음수가 아니어야 합니다
이제 사용자는 왜 입력이 잘못되었는지 정확히 알게됩니다. 심지어 try
...except
블럭을 사용하여 이를 캐치할 수 있습니다!
N = -10
try:
print("피보나치를 시도..")
print(fibonacci(N))
except ValueError:
print("잘못된 값: 무언가 다른 조치가 필요하다")
피보나치를 시도.. 잘못된 값: 무언가 다른 조치가 필요하다
간단히 앞으로 볼 수 있는 다른 개념에 대해 언급하겠습니다. 너무 자세한 개념이나 왜, 어떻게 사용하는지 설명하지 않겠습니다. 대신 간단한 사용법을 보이고 나중에 스스로 더 찾아보도록 돕겠습니다.
try
...except
문에서 이따금 에러 메세지 자체를 다루어야할 때가 있습니다.
이럴 때는 as
키워드를 사용합니다:
try:
x = 1 / 0
except ZeroDivisionError as err:
print("에러 클래스: ", type(err))
print("에러 메세지:", err)
에러 클래스: <class 'ZeroDivisionError'> 에러 메세지: division by zero
이 패턴을 사용해 함수의 예외 처리를 다양하게 커스터마이징할 수 있습니다.
기본 예외말고도 클래스 상속을 통해 독자적인 예외를 정의할 수 있습니다. 예를 들어 특별한 종류의 ValueError
가 필요하면 다음과 같이 씁니다:
class MySpecialError(ValueError):
pass
raise MySpecialError("here's the message")
MySpecialErrorTraceback (most recent call last) <ipython-input-20-1c1bb7b055e0> in <module> 2 pass 3 ----> 4 raise MySpecialError("here's the message") MySpecialError: here's the message
이 에러만 캐치하는 try
...except
블럭을 사용할 수 있습니다:
try:
print("작업 수행")
raise MySpecialError("[상세한 에러 정보]")
except MySpecialError:
print("다른 작업 수행")
작업 수행 다른 작업 수행
커스터마이징이 많이 필요한 코드일수록 이 용법이 유용합니다.
try
...except
...else
...finally
¶try
와 except
외에 else
와 finally
키워드를 사용해 예외 처리를 정교하게 튜닝할 수 있습니다.
기본적인 구조는 다음과 같습니다:
try:
print("어떤 작업을 시도합니다")
except:
print("실패할 경우 실행됩니다")
else:
print("성공할 경우 실행됩니다")
finally:
print("무조건 실행됩니다")
어떤 작업을 시도합니다 성공할 경우 실행됩니다 무조건 실행됩니다
else
의 용도는 명확하지만 finally
는 왜 필요할까요?
글쎄요, finally
절은 실제로 에러가 있던 없던 실행됩니다. 보통 어떤 작업이 완료되고 나서 자원을 정리하는 경우에 사용되곤 합니다.